<?php

namespace common\db;

use Closure;
use Yii;
use yii\db\Query;
use yii\db\Expression;
use yii\helpers\Inflector;
use yii\helpers\ArrayHelper;
use yii\behaviors\TimestampBehavior;
use yii\web\NotFoundHttpException;
use common\base\ModelHttpException;
use yii\base\InvalidConfigException;
use yii\base\InvalidParamException;
use common\behaviors\AttributeTypecastBehavior;
use common\rest\grid\DataColumn;
use common\models\shop\ShopMonitorModel;
use common\models\shop\ShopMonitorData;

/**
 * ActiveRecord is the base class for classes representing relational data in terms of objects.
 *
 * @author emhome<emhome@163.com>
 * @since 2.0
 */
class ActiveRecord extends \yii\db\ActiveRecord {

    use \common\traits\Constants;
    use \common\traits\ModelErrors;
    use \common\traits\DataPierces;
    use \common\traits\SearchForBuildQuery;

    /**
     * @inheritdoc
     */
    public $useFullField = false;

    /**
     * @inheritdoc
     */
    public $timestampAttributes = [
        'createdAtAttribute' => 'created_at',
        'updatedAtAttribute' => 'updated_at',
    ];

    /**
     * @inheritdoc
     */
    public $activeAttributes = [
        'deletedAtAttribute' => 'deleted_at',
        'activeStatusAttribute' => 'status',
    ];

    /**
     * @inheritdoc
     */
    public $typecastAttributes = [];

    /**
     * @inheritdoc
     */
    public function behaviors() {
        $behaviors = parent::behaviors();
        if (!empty($this->timestampAttributes)) {
            $timestampBehavior = [
                'class' => TimestampBehavior::class,
            ];
            foreach ($this->timestampAttributes as $key => $vls) {
                $timestampBehavior[$key] = $this->hasAttribute($vls) ? $vls : false;
            }
            $behaviors[] = $timestampBehavior;
        }

        $typecastAttributes = $this->typecastAttributes();
        if (!empty($typecastAttributes)) {
            $behaviors[] = [
                'class' => AttributeTypecastBehavior::class,
                'typecasts' => $typecastAttributes
            ];
        }
        return $behaviors;
    }

    public function typecastAttributes() {
        return $this->typecastAttributes;
    }

    /**
     * @return string the fullschema tablename.
     */
    public static function tableRawName() {
        return static::getTableSchema()->fullName;
//        return static::getDb()->getSchema()->getRawTableName(static::tableName());
    }

    /**
     * @return string the fullschema tablename.
     */
    public static function getSchemaTableName($tablename) {
        $db = static::getDb();
        preg_match("/dbname=([^;]+)/i", $db->dsn, $matches);
        return $db->getSchema()->getRawTableName($matches[1] . '.' . $tablename);
    }

    /**
     * {@inheritdoc}
     * @return ActiveQuery the newly created [[ActiveQuery]] instance.
     */
    public static function find() {
        return Yii::createObject(ActiveQuery::class, [get_called_class()]);
    }

    /**
     * {@inheritdoc}
     * @return ActiveQuery the newly created [[ActiveQuery]] instance.
     */
    public static function findAlias(&$alias = null) {
        $query = static::find();
        if ($alias) {
            $query->alias($alias);
            $alias .= '.';
        }
        return $query;
    }

    /**
     * 调用存储过程
     *
     * @param $name
     * @param array $params
     * @return \yii\db\Command
     * @throws \yii\db\Exception
     */
    public static function callProcedure($name, $params = [], $db = null) {
        if (!$db) {
            $db = static::getDb();
        }
        $sql = "CALL $name";
        $tempKeys = [];
        $inParams = [];
        $outFileds = [];
        if (!empty($params)) {
            foreach ($params as $key => $vls) {
                if (is_int($key)) {
                    $outkey = '@' . ltrim($vls, '@');
                    $tempKeys[] = $outkey;
                    $outFileds[] = $outkey;
                } elseif (strpos($key, '@') === 0) {
                    $tempKeys[] = $key;
                    $outFileds[] = $key;
                } elseif (strpos($key, ':') === 0) {
                    $tempKeys[] = $key;
                    $inParams[$key] = $vls;
                } else {
                    $tempKeys[] = ':' . $key;
                    $inParams[':' . $key] = $vls;
                }
            }
        }
        if (!empty($tempKeys)) {
            $sql .= '(' . implode(',', $tempKeys) . ')';
        }
        $sql .= ';';
        $query = $db->createCommand($sql)->bindValues($inParams);
        if (empty($outFileds)) {
            return $query;
        }
        $query->execute();
        $selects = [];
        foreach ($outFileds as $field) {
            $selects[] = $field . ' AS ' . trim($field, '@');
        }
        return $db->createCommand('SELECT ' . implode(',', $selects) . ';')->queryOne();
    }

    /**
     * @param integer $unionId 是否替换数据,如果为真, 则创建REPLACE INTO sql语句 (Only Mysql)
     * @param \yii\db\Connection $db
     * @see \yii\db\Command::batchInsert();
     */
    public static function callFunction($name, $params = [], $db = null) {
        if (!$db) {
            $db = static::getDb();
        }
        $bindParams = [];
        if (!empty($params)) {
            $params = (array) $params;
            foreach ($params as $key => $vls) {
                $_prefix = ':';
                if (is_numeric($key)) {
                    $_prefix .= 'p';
                }
                $bindParams[$_prefix . $key] = $vls;
            }
        }
        $function = $name . '(' . implode(',', array_keys($bindParams)) . ')';
        $expression = new Expression($function, $bindParams);
        return (new Query())->select($expression)->scalar($db);
    }

    /**
     * 加入记录软删除
     * {@inheritdoc}
     *
     * @param bool $hard 是否真实删除
     * @return bool|false|int
     * @throws \yii\db\StaleObjectException
     */
    public function delete($hard = false) {
        if ($hard === false && $this->hasAttribute('deleted_at')) {
            $this->setAttribute('deleted_at', time());
            return $this->save();
        }
        return parent::delete();
    }

    /**
     * 查询对象是否存在存在返回记录不存在创建
     *
     * @param mixed $condition please refer to [[findOne()]] for the explanation of this parameter
     * @return static
     */
    public static function findOrNew($condition) {
        $model = static::findOne($condition);
        if ($model === null) {
            $className = static::class;
            $model = new $className();
            if (is_array($condition)) {
                $model->setAttributes($condition);
            }
        }
        return $model;
    }

    /**
     * 查询对象是否存在存在返回记录不存在创建
     *
     * @param mixed $condition please refer to [[findOne()]] for the explanation of this parameter
     * @return static
     * @throws ModelHttpException
     */
    public static function findOrCreate($condition) {
        $model = static::findOne($condition);
        if ($model === null) {
            $model = static::instance();
            if (is_array($condition)) {
                $model->setAttributes($condition);
            }
            if (!$model->save()) {
                throw new ModelHttpException('数据更新异常', $model->getErrorMsg());
            }
        }
        return $model;
    }

    /**
     * 查询对象是否存在存在返回记录不存在返回异常
     *
     * @param mixed $condition please refer to [[findOne()]] for the explanation of this parameter
     * @return static
     * @throws NotFoundHttpException
     */
    public static function findOrFail($condition, $message = '未找到数据模型对应的记录！') {
        $model = static::findOne($condition);
        if ($model === null) {
            throw new NotFoundHttpException($message);
        }
        return $model;
    }

    /**
     * {@inheritdoc}
     * @param null $alias
     * @param bool $state
     * @return ActiveQuery the newly created [[ActiveQuery]] instance.
     * @throws InvalidConfigException
     */
    public static function findActive($alias = null, $state = false) {
        $query = static::find();
        if ($alias) {
            $query->alias($alias);
        }
        $query->active($state);
        return $query;
    }

    /**
     * {@inheritdoc}
     * @return static|null ActiveRecord instance matching the condition, or `null` if nothing matches.
     */
    public static function findActiveOne($condtion) {
        $query = static::find()->active();
        $query->andWhere($condtion);
        $model = $query->one();
        if ($model === null) {
            throw new NotFoundHttpException('未找到数据模型对应的记录！');
        }
        return $model;
    }

    /**
     * @param $class
     * @param $link
     * @param $pk
     * @return ActiveQuery|\yii\db\ActiveQuery
     */
    public function hasOneX($class, $link, $pk) {
        /* @var $class SequenceActiveRecord */
        /* @var $query ActiveQuery */
        static::log($pk);
        $query = $class::findx($pk);
        $query->primaryModel = $this;
        $query->link = $link;
        $query->multiple = false;
        return $query;
    }

    /**
     * @param $class
     * @param $link
     * @param $pk
     * @return ActiveQuery
     */
    public function hasManyX($class, $link, $pk) {
        /* @var $class SequenceActiveRecord */
        /* @var $query ActiveQuery */
        $query = $class::findx($pk);
        $query->primaryModel = $this;
        $query->link = $link;
        $query->multiple = true;
        return $query;
    }

    /**
     * Sets the named attribute value.
     *
     * @param string $name the attribute name
     * @param mixed $value the attribute value.
     * @throws InvalidArgumentException if the named attribute does not exist.
     * @see hasAttribute()
     */
    public function setGeometryAttribute($name, $value) {
        if (is_array($value) && count($value) == 2) {
            list($lng, $lat) = $value;
            //$value = new Expression("GEOMFROMTEXT('POINT($lng $lat)')");
            $value = new Expression("POINT($lng,$lat)");
        }
        $this->setAttribute($name, $value);
    }

    /**
     * 记录AR日志
     *
     * @param null $content
     */
    public static function log($content = null) {
        if (!YII_DEBUG) {
            return;
        }
        if ($content instanceof ActiveQuery) {
            $content->debug();
        } else {
            Yii::error($content, __METHOD__);
        }
    }

    /**
     * Creates a [[DataColumn]] object based on a string in the format of "attribute:format:label".
     * @param string $text the column specification string
     * @return DataColumn the column instance
     * @throws InvalidConfigException if the column specification is invalid
     */
    protected function createDataColumn($text) {
        if (!preg_match('/^([^:]+)(:(\w*))?(:(.*))?$/', $text, $matches)) {
            throw new InvalidConfigException('The column must be specified in the format of "attribute", "attribute:format" or "attribute:format:label"');
        }

        return Yii::createObject([
            'class' => \common\rest\grid\DataColumn::className(),
            'grid' => $this,
            'attribute' => $matches[1],
            'format' => isset($matches[3]) ? $matches[3] : null,
            'label' => isset($matches[5]) ? $matches[5] : null,
        ]);
    }

    /**
     * {@inheritdoc}
     */
    public function fields() {
        $fields = parent::fields();
        if (empty($fields)) {
            $fields = array_keys($this->attributes);
        }
        $columns = $this->fieldConfigs();
        if (empty($columns)) {
            return $fields;
        }
        $index = 0;
        foreach ($columns as $name => $column) {
            if (is_string($column)) {
                $column = $this->createDataColumn($column);
            } else {
                $column = Yii::createObject(array_merge([
                    'class' => DataColumn::class,
                    'grid' => $this,
                ], $column));
            }
            $attribute = $column->attribute;
            if (strrpos($attribute, '_') === 0) {
                $column->value = function($model, $attribute) use ($column) {
                    return $column->renderDataCell($model, $attribute, 0);
                };
            } elseif (strrpos($attribute, '.') !== false) {
                $column->value = function($model, $field) {
                    return ArrayHelper::getValue($model, $field);
                };
            }
            $aliasAttribute = is_numeric($name) ? $attribute : $name;

            if ($column->value instanceof Closure) {
                ArrayHelper::removeValue($fields, $aliasAttribute);
                $fields[$aliasAttribute] = $column->value;
            } elseif ($attribute) {
                if ($aliasAttribute !== $attribute) {
                    $fields[$aliasAttribute] = $attribute;
                } else {
                    $fields[] = $attribute;
                }
            }

            $index ++;
        }
        return $fields;
    }

    /**
     * @return array
     */
    public function options() {
        $options = [];
        $columns = $this->fieldConfigs();
        if (!empty($columns)) {
            foreach ($columns as $i => $column) {
                if (is_string($column)) {
                    $column = $this->createDataColumn($column);
                } else {
                    $column = Yii::createObject(array_merge([
                        'class' => DataColumn::className(),
                        'grid' => $this,
                    ], $column));
                }
                if (!$column->visible) {
                    continue;
                }
                $options[$i] = $column->renderHeaderCell();
            }
        } else {
            $fields = $this->fields();
            if (empty($fields)) {
                $fields = array_keys($this->attributes);
            }
            $configs = $this->attributeConfigs();
            foreach ($fields as $attribute) {
                $_config = ArrayHelper::getValue($configs, $attribute, []);
                if (empty($_config)) {
                    continue;
                }
                $options[] = ArrayHelper::merge([
                    'label' => $this->getAttributeLabel($attribute),
                    'prop' => $attribute,
                ], $_config);
            }
        }
        return array_values($options);
    }

    /**
     * @return array
     */
    public function gridHandles() {
        return [];
    }

    /**
     * @return array
     */
    public function attributeConfigs() {
        return [];
    }

    /**
     * @return array|DataColumn[]
     */
    public function fieldConfigs() {
        return [];
    }

    /**
     * @return array
     */
    public function getFilterAttributes() {
        return [];
    }

    /**
     * Returns the attribute hints.
     *
     * Attribute hints are mainly used for display purpose. For example, given an attribute
     * `isPublic`, we can declare a hint `Whether the post should be visible for not logged in users`,
     * which provides user-friendly description of the attribute meaning and can be displayed to end users.
     *
     * Unlike label hint will not be generated, if its explicit declaration is omitted.
     *
     * Note, in order to inherit hints defined in the parent class, a child class needs to
     * merge the parent hints with child hints using functions such as `array_merge()`.
     *
     * @return array attribute hints (name => hint)
     * @since 2.0.4
     */
    public function attributeFormats() {
        return [];
    }

    /**
     * Returns the text label for the specified attribute.
     * If the attribute looks like `relatedModel.attribute`, then the attribute will be received from the related model.
     * @param string $attribute the attribute name
     * @return string the attribute label
     * @see generateAttributeLabel()
     * @see attributeLabels()
     */
    public function getAttributeFormat($attribute) {
        $formats = $this->attributeFormats();
        if (isset($formats[$attribute])) {
            return $formats[$attribute];
        } elseif (strpos($attribute, '.')) {
            $attributeParts = explode('.', $attribute);
            $neededAttribute = array_pop($attributeParts);

            $relatedModel = $this;
            foreach ($attributeParts as $relationName) {
                if ($relatedModel->isRelationPopulated($relationName) && $relatedModel->$relationName instanceof self) {
                    $relatedModel = $relatedModel->$relationName;
                } else {
                    try {
                        $relation = $relatedModel->getRelation($relationName);
                    } catch (InvalidParamException $e) {
                        return '';
                    }
                    /* @var $modelClass ActiveRecordInterface */
                    $modelClass = $relation->modelClass;
                    $relatedModel = $modelClass::instance();
                }
            }

            $formats = $relatedModel->attributeFormats();
            if (isset($formats[$neededAttribute])) {
                return $formats[$neededAttribute];
            }
        }
        return 'raw';
    }

    /**
     * Returns attribute values.
     *
     * @param array|null $names list of attributes whose value needs to be returned.
     * Defaults to null, meaning all attributes listed in [[attributes()]] will be returned.
     * If it is an array, only the attributes in the array will be returned.
     * @param array $except list of attributes whose value should NOT be returned.
     * @return array attribute values (name => value).
     * @return array
     */
    public function getCamelAttributes($names = null, $except = array()) {
        $values = [];
        if ($names === null) {
            $names = $this->attributes();
        }
        foreach ($names as $name) {
            $key = Inflector::variablize($name, '_');
            $values[$key] = $this->$name;
        }
        foreach ($except as $name) {
            $key = Inflector::variablize($name, '_');
            unset($values[$key]);
        }
        return $values;
    }

    /**
     * @param $attribute
     * @return string
     */
    protected function buildCachePrefix($attribute) {
        $primaryKey = $this->primaryKey;
        $cacheName = array_filter([
            static::class,
            $attribute,
            $primaryKey,
        ]);
        return implode('_', $cacheName);
    }

    /**
     * @param null $attribute
     * @param int $duration
     * @return array|mixed|null
     */
    public function getCacheAttributes($attribute = null, $duration = 1800) {
        $cache = static::getCommonCache();
        $cacheName = $this->buildCachePrefix($attribute);
        $data = $cache->get($cacheName);
        if ($data !== false) {
            return $data;
        }
        if ($attribute === null) {
            $data = $this->attributes;
        } else {
            $method = 'get' . ucfirst($attribute);
            $handler = array($this, $method);
            if ($this->hasMethod($method) && is_callable($handler)) {
                $data = call_user_func_array($handler, [$this]);
            } elseif ($this->hasAttribute($attribute)) {
                $data = $this->getAttribute($attribute);
            } else {
                $data = false;
            }
        }
        if ($data !== false) {
            $cache->set($cacheName, $data, $duration);
        }
        return $data;
    }

    /**
     * {@inheritdoc}
     */
    public function getIdentifyUser() {
        if (!Yii::$app->user->isGuest) {
            return Yii::$app->user->identity;
        }
        return null;
    }

    /**
     * {@inheritdoc}
     */
    public function isActived($state = null) {
        if ($this->hasAttribute('deleted_at')) {
            $deleted = boolval($this->getAttribute('deleted_at'));
            if ($deleted) {
                return !$deleted;
            }
        }
        if ($state !== null && $this->hasAttribute('status')) {
            $status = $this->getAttribute('status');
            if ($status != $state) {
                return false;
            }
        }
        return true;
    }

    /**
     * {@inheritdoc}
     */
    protected function monitor($insert, $changedAttributes, $model) {
        $columns = array_keys($changedAttributes);
        $monitors = ShopMonitorModel::find()->andWhere([
            'model' => $model::tableName(),
            'status' => ShopMonitorModel::STATUS_ACTIVE,
            'field' => $columns,
        ])->all();
        foreach ($monitors as $monitor) {
            $monitorData = ShopMonitorData::findOrNew([
                'monitor_id' => $monitor->monitor_id,
                'monitor_model_id' => $monitor->id,
                'model_id' => ArrayHelper::getValue($model, $monitor->model_key, 0),
                'model_pk' => ArrayHelper::getValue($model, 'id', 0),
                'field' => $monitor->field,
            ]);
            $monitorData->setAttributes([
                'before' => ArrayHelper::getValue($changedAttributes, $monitor->field),
                'after' => ArrayHelper::getValue($model, $monitor->field),
            ]);
            $monitorData->save(false);
        }
    }

}
